/* Editor.cpp
Copyright (c) 2008 Michael Zahniser
Please note the license terms (MIT license) at the end of this document.

Function definitions for the kwirki editor class.
*/

#include "Editor.h"

#include "_common/wrap.h"
#include "_paste/Pasteboard.h"

#include <fstream>
#include <cstdlib>
#include <algorithm>
#include <cctype>

using std::ifstream;
using std::ofstream;
using std::getenv;
using std::upper_bound;
using std::lower_bound;
using std::copy;
using std::copy_backward;



// Constructor, specifying how the text width should be measured.
Editor::Editor(const ViewProperties<text> &viewProperties)
	: view(viewProperties)
{
	Initialize();
}



// Load the given file.
void Editor::Open(const string &fileName)
{
	this->fileName = fileName;
	filePath = RootDirectory() + fileName + ".txt";

	ifstream in(filePath.c_str());
	in >> file;
	
	Initialize();
}



// Save the current file.
void Editor::Save() const
{
	if(!file.empty())
	{
		// If necessary, create the directory path this file is in.
		size_t pos = filePath.rfind('/');
		while(pos != string::npos)
		{
			if(pos < filePath.length() - fileName.length())
				break;
			string command = "mkdir -p " + filePath.substr(0, pos);
			int error = system(command.c_str());
			if(error)
				return;
			pos = filePath.rfind('/', pos - 1);
		}

		ofstream out(filePath.c_str());
		out << file;
	}
}



// Get the current file's name.
const string &Editor::FileName() const
{
	return fileName;
}



// Get iterators to the beginnings of all the lines of text to display.
// If the given row is out of bounds, the end of the file is returned.
text::const_iterator Editor::Line(int row) const
{
	// Make sure the lines are populated at least as far as the cursor.
	if(lines.empty())
		lines.push_back(file.make_mark(file.begin()));
	text::mark mk = file.make_mark(cursor);
	while(!(mk < lines.back()) && WrapNextLine()) {}
	PreScroll(0);

	// Scroll so that the cursor is in view.
	vector<text::mark>::const_iterator it = CursorLineIt();
	vector<text::mark>::const_iterator last = lines.begin() + scrollLine + view.lineCount;	
	if(it >= last)
	{
		// Scroll in increments of four lines.
		scrollLine += ((it - last) / 4 + 1) * 4;
		PreScroll(0);
		// Do not scroll beyond the end of the file.
		if(scrollLine > static_cast<int>(lines.size()) - view.lineCount)
			scrollLine = lines.size() - view.lineCount;
	}
	vector<text::mark>::const_iterator first = lines.begin() + scrollLine;	
	if(it < first)
	{
		// Scroll in increments of four lines.
		scrollLine -= (((first - it) + 3) / 4) * 4;
		if(scrollLine < 0)
			scrollLine = 0;
	}
	
	// Update the cursor desired X, (if requested by setting it negative).
	if(cursorDesiredX < 0)
		cursorDesiredX = view.measure(file.make_iterator(*it), cursor);

	// Figure out which row we're starting at.
	row += scrollLine;
	if(static_cast<size_t>(row) >= lines.size())
		return file.end();
	
	return file.make_iterator(lines[row]);
}



// Get the index of the line that the cursor is on.
int Editor::CursorLine() const
{
	return static_cast<int>(CursorLineIt() - lines.begin()) - scrollLine;
}



// Get the X position of the cursor within that line.
int Editor::CursorX() const
{
	return view.measure(file.make_iterator(*CursorLineIt()), cursor);
}


	
// Handle the given input. The key code is in ncurses form.
void Editor::HandleKey(int key)
{
	// Handle kill of multiple lines (append onto the pasteboard).
	static bool isKilling = false;
	isKilling &= (key == 11);
	
	if(key == 11)
	{
		// ^K is kill.
		// Cut everything up to the next newline.
		text::iterator it = file.end();
		vector<text::mark>::const_iterator lineIt = ++CursorLineIt();
		if(lineIt != lines.end())
			it = file.make_iterator(*lineIt);
		string cut(cursor, it);
		
		string clip;
		if(isKilling)
			clip = Pasteboard::Get();
		isKilling = true;
		
		Pasteboard::Set(clip + cut);
		
		cursor = file.erase(cursor, it);
		// Re-wrap everything after the deletion.
		ClearWrapping(cursor);
	}
	else if(key == 21)
	{
		// ^U is unkill / yank.
		// Used in order to match pico.
		
		const string &clip = Pasteboard::Get();
		if(!clip.empty())
		{
			// Re-wrap everything after the inserted string.
			ClearWrapping(cursor);
			
			// Insert the character at the cursor position.
			cursor = file.insert(clip.data(), clip.length(), cursor);
			// Recalculate cursor desired X.
			cursorDesiredX = -1;
		}
	}
	else if(key == 260)
	{
		// Left arrow.
		if(cursor == file.begin())
			return;
		
		--cursor;
		// Recalculate cursor desired X.
		cursorDesiredX = -1;
	}
	else if(key == 261)
	{		
		// Right arrow.
		if(cursor == file.end())
			return;
		
		++cursor;
		// Recalculate cursor desired X.
		cursorDesiredX = -1;
	}
	else if(key == 259)
	{
		// Up arrow.
		CursorUp(1);
	}
	else if(key == 258)
	{
		// Down arrow.
		CursorDown(1);
	}
	else if(key == '\t')
	{
		// Tab key. Go to next link (if any).
		NextLink();
	}
	else if(isgraph(key) || isspace(key))
	{
		// Re-wrap everything after the inserted character.
		ClearWrapping(cursor);
		
		// Insert the character at the cursor position.
		cursor = file.insert(key, cursor);
		// Recalculate cursor desired X.
		cursorDesiredX = -1;
	}
	else if(key == 127)
	{
		cursor = file.backspace(cursor);
		// Recalculate cursor desired X.
		cursorDesiredX = -1;
		
		// Re-wrap everything after the deleted character.
		ClearWrapping(cursor);
	}
	else if(key == 25)
	{
		// ^Y: scroll up. Just go to the start of the text if it fits on one page,
		// or if we are at the top.
		CursorUp(view.lineCount - 2);
	}
	else if(key == 22)
	{
		// ^V: scroll down. Just go to the end of the text if it fits on one page,
		// or if we are at the bottom.
		CursorDown(view.lineCount - 2);
	}
}


	
// Get the text of the link that the cursor is over (if any).
const string &Editor::LinkText() const
{
	static string text;
	text.clear();
	
	text::const_iterator first = file.rfind("[[", cursor);
	text::const_iterator last = file.find("]]", first);
	if(first == file.end() || last == file.end() || last + 2 <= text::const_iterator(cursor))
		return text;

	// The link name starts two characters after first.
	first += 2;
	text = string(first, last);
	return text;
}



// Move the cursor to the named anchor within this page, if it exists.
void Editor::GoToAnchor(const string &name)
{
	string anchor = "## " + name + " ##";
	text::iterator it = file.find(anchor.c_str());
	
	if(it != file.end())
	{
		cursor = it;
		// Recalculate cursor desired X.
		cursorDesiredX = -1;
	}
}



// Initialize this editor (no line wrapping calculated, scrolled to the top,
// cursor at the top of the document).
void Editor::Initialize()
{
	scrollLine = 0;
	cursor = file.begin();
	cursorDesiredX = 0;
	
	// Clear the previous line breaks, if any.
	ClearWrapping(file.begin());
}



// Clear all the line wrapping data after the given point.
void Editor::ClearWrapping(text::const_iterator it) const
{
	if(it == file.begin())
		lines.clear();
	else
	{
		// TODO: When a mark is deleted, its position becomes -1. This upsets
		// the ordering of the lines vector, which means that lower_bound may
		// fail (because it expects a sorted list). Fix this, somehow.
		
		// Temporary fix: Use an O(n) linear search instead of an O(lg(n))
		// lower_bound search? Or, just stick with upper_bound, which seems
		// to work (at least on my Mac) but may not be portable.
		
		// Lower_bound returns where (*it < value) == false, so it will get
		// messed up if *it == -1.
		// Upper_bound narrows the search looking for (value < *it) == true.
		// So if it hits a -1, it incorrectly continues to the left,
		// removing more lines than are necessary (instead of fewer lines).
		// That is an acceptable behavior and is still better than O(n).
		
		// In other words, all the algorithm sees is the value of (value < *it).
		// Assume that the -1s all fall right at the beginning of the range that
		// we want to delete.
		
		// Other solution: have mark::operator< always return false if mark is
		// undefined.
		
		// Convert the iterator into a mark.
		// First move the iterator to the other side of the gap, to be sure
		// to delete anything inside the gap.
		text::mark mk = file.make_mark(it);
		
		// Do this the safe way, iterating forward from the beginning of the lines.
		vector<text::mark>::iterator lineIt = lines.begin();
		for( ; lineIt < lines.end() && *lineIt < mk; ++lineIt) {}
		
		// Delete an extra line, in case editing inserted a space that allows a break
		// where one was not possible before.
		if(lineIt != lines.begin())
			--lineIt;
		
		// Now, !(*lineIt < mk).
		lines.erase(lineIt, lines.end());
	}
}



// Make sure that we have line wrap marks for at least the given number of 
// lines below the bottom of the display, unless at the end of the file.
void Editor::PreScroll(int lineCount) const
{
	if(lines.empty())
		lines.push_back(file.make_mark(file.begin()));
	
	size_t goalLine = scrollLine + view.lineCount + lineCount;
	
	// Scroll until either:
	// 1. lines.size() > scroll + view.lineCount + lineCount, or
	// 2. lines.back() == file.end().
	while(lines.size() <= goalLine && WrapNextLine()) {}
}



// Add one line to the line wrap vector. Return false if we're at the end of
// the file.
bool Editor::WrapNextLine() const
{
	text::const_iterator it = file.make_iterator(lines.back());
	it = wrap(it, file.end(), view.measure, view.lineWidth);
	if(it > file.end())
		return false;
	
	lines.push_back(file.make_mark(it));
	return true;
}



	
// Move the cursor to the desired X.
// Assume it >= lines.begin() and it < lines.end() - 1.
void Editor::CursorToDesiredX(vector<text::mark>::const_iterator it)
{
	cursor = file.make_iterator(*it);
	++it;
	text::iterator start = cursor;
	text::iterator end = file.end();
	if(it != lines.end())
		end = file.make_iterator(*it);
	// If we're at the end of the file, use the true end as the end of line.
	end -= (end != file.end());
	while(cursor != end && view.measure(start, cursor + 1) <= cursorDesiredX)
		++cursor;
}



// Get an iterator to the line break entry for the line the cursor is in.
vector<text::mark>::iterator Editor::CursorLineIt()
{
	return --upper_bound(lines.begin(), lines.end(), file.make_mark(cursor));
}



// Get an iterator to the line break entry for the line the cursor is in.
vector<text::mark>::const_iterator Editor::CursorLineIt() const
{
	return --upper_bound(lines.begin(), lines.end(), file.make_mark(cursor));
}



// Scroll the view up by the given number of lines, maintaining line wrapping.
// It may not scroll as far as specified if the end of file is reached.
void Editor::CursorUp(int lineCount)
{
	// If the cursor is at the very end of the file, it's here because we
	// tried to go down when already on the last line.
	if(cursor == file.end() && cursorDesiredX < CursorX())
		--lineCount;
	
	// Get the line the cursor is on.
	vector<text::mark>::iterator it = CursorLineIt();
	it -= lineCount;
	
	if(it < lines.begin())
		cursor = file.begin();
	else
		CursorToDesiredX(it);
}



// Scroll the view down by the given number of lines, maintaining line wrapping.
// It may not scroll as far as specified if the end of file is reached.
void Editor::CursorDown(int lineCount)
{
	// Make sure there's at least one line below the screen to scroll down to.
	PreScroll(lineCount);
	
	// If the cursor is at the very beginning of the file, it's here because we
	// tried to go up when already on the first line.
	if(cursor == file.begin() && cursorDesiredX)
		--lineCount;
	
	// Get the line the cursor is on.
	vector<text::mark>::iterator it = CursorLineIt();
	it += lineCount;
	
	// If we're on the last line, move to the last character.
	if(it >= lines.end())
		cursor = file.end();
	else
		CursorToDesiredX(it);
}



// Move the cursor to the next link, scrolling as necessary.
// If no links are found, keep the cursor where it is.
void Editor::NextLink()
{
	text::iterator it = file.find("[[", cursor + 1);
	if(it == file.end())
		it = file.find("[[", file.begin());
	if(it != file.end())
	{
		cursor = it;
		// Recalculate cursor desired X.
		cursorDesiredX = -1;
	}
}



const string &Editor::RootDirectory()
{
	static string rootDirectory;
	
	if(rootDirectory.empty())
	{
		rootDirectory = getenv("HOME");
		ifstream in((rootDirectory + "/.kwirki").c_str());
		if(in)
			getline(in, rootDirectory);
		rootDirectory += "/.kwirki.d/";
	}
	
	return rootDirectory;
}




/* Copyright (c) 2008 Michael Zahniser

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE. */

